個人CodeBase紀錄 - EP.2 不想 Bind data 到吐,來自訂一下 Aspose 的擴充


Posted by Mike.Lin on 2023-08-07

公司專案的需求,常有將資料套進範例檔或是做報表的需求,若是每次套版都要重新寫迴圈、Bind資料,會吐,真的會吐TT。所以這篇除了aspose基本的使用,主軸會放在自訂擴充方法的部分。


首先,先來說明aspose。Aspose是一個開發文檔控制相關套件的公司,需要購買憑證才能有完整功能,套件提供了許多操控文檔結構、資料及樣式的實作。以下我會說明 .doc 及 .xlsx 在aspose中如何實作 bind data:

Aspose.Words

文檔控制在實作上,主要是找到對應欄位或位置,並對該位置作改動,而文檔會有不同的結構,如cells、tables、prograph等,不同的結構在Asp.Net可以當作物件,同時aspose提共了方法來操作這些結構。

doc文檔中,我們可以透過 Mergefield 來找到目標位置在文檔中的位置。如圖中所示,可以使用 word 中的插入功能變數來新增一個MergeField

並撰寫程式如下,將範例檔欄位名稱與資料作對應

Document doc = new Document("template.docx"); // 取得範例檔

// new 一個 DataTable 物件來接收資料
var table = new DataTable();
table.Columns.Add("UserName", string); // 給欄位名稱及型別
var row = table.NewRow(); 
row[0] = "Mike" // 給值
table.Rows.Add(row); // 加回 DataTable

// 進行郵件合併
doc.MailMerge.Execute(table);

// 存檔
doc.Save("merged.docx");

結果如下,可以看到UserName已經被取代為"Mike"

我們也可以透過bookmark的方式來控制文檔,但這部分並非此篇重點,就不細談。

Aspose.Cells

xlsx 文檔中,則是透過 "&=" 字符來標示欄位,後面接 table Name 及 colum Name

並撰寫程式如下,將範例檔欄位名稱與資料作對應

var workBook = new Workbook("test.xlsx"); // 取得範例檔
var designer = new WorkbookDesigner(); // designer 物件 用以處理文檔
designer.Workbook = workBook;

var test = new DataTable();
/*
datatable 部分
*/

// 給定 tablename
test.TableName = "data"; 

var dataSet = new DataSet(); 
dataSet.Tables.Add(test); // 將 datatable塞到dataset
designer.SetDataSource(dataSet); // 透過 designer 來確認資料來源

designer.Process(true); // binding

值得注意的是,xlsx 文檔內 所有物件的上層都會有 table,因此必須給範例檔中相同的 table name 才對應的到。

Bind 完會自動依資料筆數取代字符。資料及結果如下:

自訂擴充方法

在實作時,我們通常會有不同區塊的欄位需要填寫,有時某些資料會是列表,需要動態去作欄位的新增,所以共用方法必須考慮可以去解析.Net資料的結構,特別是像葡萄串一樣的資料。


DataTable

我預設將接近來的資料分為單筆及多筆,用不同方式去處理。

單筆部分

只取非泛型、非列表的欄位,並根據這些欄位及資料塞入datatable中

/// <summary>
/// 單一field bind 後回傳table
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
private static DataTable GetBindDataSingleField<T>(T data)
{
    var table = new DataTable();
    // 取得資料的 名稱、屬性 列表 // 列表內資料非泛型、非列表
    var columnFieldList = data.GetType().GetProperties().Where(p => !(p.PropertyType.IsGenericType && p is IList)).Select(x => new { x.Name, x.PropertyType }).ToList();
    foreach (var column in columnFieldList)
    {
        // 塞到欄位中 類型為基礎類型或屬性的類型
        table.Columns.Add(column.Name, Nullable.GetUnderlyingType(column.PropertyType) ?? column.PropertyType);
    }

    var row = table.NewRow();
    // 取得資料的 名稱、值  列表
    var dataMapping = data.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(data, null)).ToList();
    // 取得資料的 值  列表
    var columnNameList = columnFieldList.Select(m => m.Name).ToList();
    foreach (var item in dataMapping)
    {
        // 名稱若有對到
        if (columnNameList.Any(x => x == item.Key))
        {
            // 名稱對應的row 塞值 // 找不到報null錯
            row[item.Key] = item.Value ?? DBNull.Value;
        }
    }
    table.Rows.Add(row);

    return table;
}

非單筆部分

逐筆處理資料為泛型、列表的資料,一樣根據這些欄位及資料塞入datatable中

/// <summary>
/// 列表field bind 後回傳table
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="fillNewRowWhenEmpty"></param>
/// <returns></returns>
private static List<DataTable> GetBindDataListField<T>(T data, bool fillNewRowWhenEmpty)
{
    var tables = new List<DataTable>();
    // 取得資料的 名稱、屬性 列表 // 列表內資料為泛型、列表
    var listTypeFields = data.GetType().GetProperties().Where(p => p.PropertyType.IsGenericType && p.GetValue(data, null) is IList).ToList();

    foreach (var item in listTypeFields)
    {
        var table = new DataTable
        {
            TableName = item.Name
        };
        // 取得屬性的類型泛型 非單一時報錯
        var type = item.PropertyType.GetGenericArguments().Single();
        // 取得資料的 名稱、屬性 列表
        var columnFieldList = type.GetProperties().Select(x => new { x.Name, x.PropertyType }).ToList();
        // List<T> 中取得 T 的 屬性名稱、類型
        foreach (var column in columnFieldList)
        {
            table.Columns.Add(column.Name, Nullable.GetUnderlyingType(column.PropertyType) ?? column.PropertyType);
        }

        // 取得列表值
        var rowDataSource = (IList)item.GetValue(data, null);

        foreach (var rowDataItemObj in rowDataSource)
        {
            var rowDataItem = Convert.ChangeType(rowDataItemObj, type);
            // 取得資料的 名稱、值  列表
            var dataMapping = rowDataItem.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(rowDataItem, null)).ToList();
            var row = table.NewRow();
            foreach (var fieldData in dataMapping)
            {
                // 名稱若有對到
                if (columnFieldList.Any(x => x.Name == fieldData.Key))
                {
                    // 名稱對應的row 塞值 // 找不到報null錯
                    row[fieldData.Key] = fieldData.Value ?? DBNull.Value;
                }
            }
            table.Rows.Add(row);
        }
        if (table.Rows.Count == 0 && fillNewRowWhenEmpty)
        {
            table.Rows.Add(table.NewRow());
        }
        tables.Add(table);
    }
    return tables;
}

完成以上兩個function,我們便有了可以將資料轉成 DataTable 的方法。接下來,由於 docx 與 xlsx 實作 binding 方式的不同,這邊分開來寫:

Document

/// <summary>
/// bind doc中同名欄位
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="doc"></param>
/// <param name="data"></param>
/// <param name="fillNewRowWhenEmpty"></param>
/// <returns></returns>
public static Document BindData<T>(this Document doc, T data, bool fillNewRowWhenEmpty = false)
{
    #region 郵件合併 doc內只能用 功能變數 -> MergyField
    // Data 類別第一層
    // 取得單一field bind後table
    var tableSingleField = GetBindDataSingleField(data);
    // doc跟bind後table合併
    doc.MailMerge.Execute(tableSingleField);

    // Data 類別第二層(List內的List)
    // 取得列表 field bind後table
    var tables = GetBindDataListField(data, fillNewRowWhenEmpty);
    // doc跟bind後table合併
    foreach (var item in tables)
    {
        doc.MailMerge.ExecuteWithRegions(item);
    }
    doc.Save("Test.docx", Aspose.Words.SaveFormat.Docx);
    #endregion
    #region ReportingEngine 可用 <<[]>> 較靈活 較簡單
    // 將模板文檔與生成report
    var engine = new ReportingEngine();
    engine.BuildReport(doc, data);
    #endregion
    return doc;
}

Workbook

/// <summary>
/// 把資料合併到 workBook 中同名的㯗位
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="doc"></param>
/// <param name="data"></param>
/// <returns></returns>
public static Workbook BindData<T>(this Workbook workBook, T data, bool fillNewRowWhenEmpty = true)
{
    var designer = new WorkbookDesigner();
    designer.Workbook = workBook;

    var dataSet = new DataSet();
    // Data 類別第一層
    // 取得單一field bind後table
    var test = GetBindDataSingleField(data);
    // 多設table名與第一層資料對應
    test.TableName = "data";

    dataSet.Tables.Add(test);
    designer.SetDataSource(dataSet);

    // Data 類別第二層(List內的List)
    // 取得列表 field bind後table
    var listTables = GetBindDataListField(data, fillNewRowWhenEmpty);
    dataSet.Tables.AddRange(listTables.ToArray());
    // set回 workbook
    designer.SetDataSource(dataSet);
    // binding // 保留計算並生成excel
    designer.Process(true);

    return workBook;
    }

使用自訂擴充作 Data Binding

完成以上,我們便可在web專案只考慮資料及範例檔,就能完成套印

/// <summary>
/// 取得doc檔匯出串流
/// </summary>
/// <returns></returns>
public ActionResult ExportDocxFile()
{
    // 假資料
    var data = _FakeMultiLayerList.FakeListForBind(); // 假資料

    var doc = new Document("test.docx"); // 開啟範例文檔
    doc.BindData(data); // 使用 自訂bind擴充
    doc.Save("bindedDoc.docx", Aspose.Words.SaveFormat.Docx);

    return File(doc.GetFileStream(Aspose.Words.SaveFormat.Docx), "application/docx");
}

/// <summary>
/// 取得xlsx檔匯出串流
/// </summary>
/// <returns></returns>
public ActionResult ExportXlsxFile()
{
    var data = _FakeMultiLayerList.FakeListForBind(); // 假資料

    var workBook = new Workbook("test.xlsx"); // 開啟範例文檔
    workBook.BindData(data); // 使用 自訂bind擴充
    workBook.Save("bindedDoc.xlsx", Aspose.Cells.SaveFormat.Xlsx);

    return File(workBook.GetFileStream(Aspose.Cells.SaveFormat.Xlsx), "application/xlsx");
}

結語

這邊的擴充,是考慮了我在工作上常用到的檔案套印,資料通常都是較扁平的,並且多數沒有作樣式的調整(樣式的部分通常為客製的,很難寫成通用方法),若先把擴充寫好,便只需考慮範例文檔的調整,省了很多事。


#ASP.NET #aspose.words #Aspose.Cells #notes







Related Posts

<form> Attributes

<form> Attributes

[15] 值 - NaN、Infinity

[15] 值 - NaN、Infinity

r3:0 異世界網站挑戰 - 破關紀錄

r3:0 異世界網站挑戰 - 破關紀錄


Comments